package elicecn.codegen;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Serializable;
import java.io.Writer;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.sql.DataSource;

import freemarker.template.Configuration;
import freemarker.template.DefaultObjectWrapper;
import freemarker.template.Template;
import freemarker.template.TemplateException;

/**
 * 代码生成类
 * 
 * @author 屹砾
 * @email eli.wuhan@gmail.com
 * @time 2009-1-20
 */
@SuppressWarnings("unchecked")
public class CodeGenerator {
	private DataSource dataSource;
	private Connection connection;

	// 数据库类型对应的Java类型
	private static Map<String, Class> TYPE = null;
	// freemarker 配置对象
	private static Configuration cfg = null;

	// 获取当前用户所有的表
	protected String selectTables = "SELECT TABLE_NAME FROM USER_TABLES";
	// 获取指定表名所有的字段
	// SELECT COLUMN_NAME, DATA_TYPE, DATA_SCALE FROM USER_TAB_COLUMNS WHERE
	// TABLE_NAME=?
	protected String selectColumns = "SELECT COLUMN_NAME, DATA_TYPE, DATA_SCALE FROM USER_TAB_COLUMNS WHERE TABLE_NAME=?";
	// 获取指定表名的惟一主键
	protected String selectTablePK = "SELECT A.COLUMN_NAME FROM USER_CONS_COLUMNS A	INNER JOIN USER_CONSTRAINTS B ON A.OWNER=B.OWNER AND A.CONSTRAINT_NAME=B.CONSTRAINT_NAME AND B.CONSTRAINT_TYPE='P' WHERE B.TABLE_NAME=?";

	// sqlmap 文件夹名
	protected String sqlmapFolder = "sqlmap";
	protected String springFolder = "spring";

	// 存储所有的表信息
	private List<TableClass> dataTables = new ArrayList<TableClass>(0);

	static {
		TYPE = new HashMap<String, Class>(0);
		TYPE.put("NUMBER", Long.class);
		TYPE.put("DATE", Date.class);
		TYPE.put("VARCHAR2", String.class);
		TYPE.put("BLOB", String.class);
		TYPE.put("CLOB", String.class);
		TYPE.put("LONG", String.class);
		TYPE.put("RAW", String.class);
		cfg = new Configuration();
	}

	/**
	 * 初始化
	 * 
	 * @throws SQLException
	 * @throws IOException
	 */
	public void init() throws SQLException, IOException {
		connection = dataSource.getConnection();
		cfg.setDirectoryForTemplateLoading(new File(CodeGenerator.class
				.getResource("").getPath()));
		cfg.setObjectWrapper(new DefaultObjectWrapper());
	}

	/**
	 * 获得所有的用户表名
	 * 
	 * @param basePackageName
	 *            基本包名
	 * @param ignoreUnderline
	 *            字段转换成类成员时是否忽悠字段中的下划线
	 * @return 用户表名
	 * @throws SQLException
	 *             数据库连接异常
	 */
	protected void fetchDatabaseTables(String basePackageName,
			String baseStructPackageName, boolean ignoreUnderline)
			throws SQLException {
		Statement stt = connection.createStatement();
		ResultSet rs = stt.executeQuery(selectTables);

		String tableName = null;
		TableClass tableClass = null;
		while (rs.next()) {
			tableName = rs.getString("TABLE_NAME");
			tableClass = fetchTableClass(tableName, basePackageName,
					baseStructPackageName, ignoreUnderline);
			dataTables.add(tableClass);
		}
		rs.close();
		stt.close();
	}

	/**
	 * 获得表的字段名和类型名
	 * 
	 * @param tableName
	 *            表名
	 * @param ignoreUnderline
	 *            是否忽悠字段中的下划线
	 * @return 字段名和类型名集合
	 * @throws SQLException
	 *             数据库连接异常
	 */
	protected TableClass fetchTableClass(String tableName,
			String basePackageName, String baseStructPackageName,
			boolean ignoreUnderline) throws SQLException {
		TableClass tableClass = new TableClass();
		tableClass.setTableName(tableName);
		tableClass.setBasePackageName(basePackageName);
		tableClass.setBaseStructPackageName(baseStructPackageName);
		tableClass.setClassName(convertClassName(tableName));

		PreparedStatement pst = null;
		ResultSet rs = null;

		// 提取表字段和类型
		pst = connection.prepareStatement(selectColumns);
		pst.setString(1, tableName);
		rs = pst.executeQuery();
		Set<Class<Serializable>> importClasses = new HashSet<Class<Serializable>>(
				0);
		ColumnField col = null;
		String colName = null, colType = null, dataScale =null;
		Class clazz = null;
		while (rs.next()) {
			colName = rs.getString("COLUMN_NAME");
			colType = rs.getString("DATA_TYPE");
			dataScale = rs.getString("DATA_SCALE");
			if (colType.equals("NUMBER") && !dataScale.equals("0")) {
				clazz = Double.class;
			} else {
				clazz = TYPE.get(colType);
			}
			col = new ColumnField(colName, colType, convertClassField(colName,
					ignoreUnderline), clazz);
			tableClass.getColumns().add(col);
			if (!clazz.getName().startsWith("java.lang")) {
				importClasses.add(clazz);
			}
		}
		tableClass.getImportClasses().addAll(importClasses);
		rs.close();
		pst.close();

		// 提取主键
		pst = connection.prepareStatement(selectTablePK);
		pst.setString(1, tableName);
		rs = pst.executeQuery();
		if (rs.next()) {
			String pkColumnName = rs.getString("COLUMN_NAME");
			tableClass.setPkColumnName(pkColumnName);
			tableClass.setPkFieldName(convertClassField(pkColumnName,
					ignoreUnderline));
		}
		rs.close();
		pst.close();

		return tableClass;
	}

	/**
	 * 生成代码类
	 * 
	 * @param basePackageName
	 *            包名
	 * @param basePath
	 *            保存路径
	 * @param ignoreUnderline
	 *            是否忽悠字段中的下划线
	 * @throws IOException
	 * @throws TemplateException
	 * @throws SQLException
	 */
	public void buildCodes(String basePackageName,
			String baseStructPackageName, String basePath,
			boolean ignoreUnderline) throws IOException, TemplateException,
			SQLException {
		fetchDatabaseTables(basePackageName, baseStructPackageName,
				ignoreUnderline);

		Configuration cfg = new Configuration();
		cfg.setDirectoryForTemplateLoading(new File(CodeGenerator.class
				.getResource("").getPath()));
		cfg.setObjectWrapper(new DefaultObjectWrapper());
		Map root = null;

		mkdirs(basePath, basePackageName + ".model");
		mkdirs(basePath, basePackageName + ".dto");
		mkdirs(basePath, basePackageName + ".dao");
		mkdirs(basePath, basePackageName + ".manager");
		mkdirs(basePath, sqlmapFolder);
		mkdirs(basePath, springFolder);

		File file = null;
		List<String> sqlmap = new ArrayList<String>(0);
		for (TableClass tableClass : dataTables) {
			root = new HashMap(0);
			root.put("tableClass", tableClass);
			// 把代码写入类文件
			file = new File(basePath + File.separator
					+ basePackageName.replace(".", File.separator)
					+ File.separator + "model" + File.separator
					+ tableClass.getClassName() + ".java");
			writeTemplate(root, file, "ftl/model.ftl");
			// DTO
			file = new File(basePath + File.separator
					+ basePackageName.replace(".", File.separator)
					+ File.separator + "dto" + File.separator
					+ tableClass.getClassName() + "Dto.java");
			writeTemplate(root, file, "ftl/dto.ftl");
			// DAO
			file = new File(basePath + File.separator
					+ basePackageName.replace(".", File.separator)
					+ File.separator + "dao" + File.separator
					+ tableClass.getClassName() + "Dao.java");
			writeTemplate(root, file, "ftl/dao.ftl");
			// DAOIMPL
			file = new File(basePath + File.separator
					+ basePackageName.replace(".", File.separator)
					+ File.separator + "dao" + File.separator
					+ tableClass.getClassName() + "DaoImpl.java");
			writeTemplate(root, file, "ftl/daoImpl.ftl");
			// SERVICE
			file = new File(basePath + File.separator
					+ basePackageName.replace(".", File.separator)
					+ File.separator + "manager" + File.separator
					+ tableClass.getClassName() + "Manager.java");
			writeTemplate(root, file, "ftl/manager.ftl");
			// SERIVCEIMPL
			file = new File(basePath + File.separator
					+ basePackageName.replace(".", File.separator)
					+ File.separator + "manager" + File.separator
					+ tableClass.getClassName() + "ManagerImpl.java");
			writeTemplate(root, file, "ftl/managerImpl.ftl");
			// IBATIS sqlmap 配置文件
			file = new File(basePath + File.separator + sqlmapFolder
					+ File.separator + "sqlmap_" + tableClass.getClassName()
					+ ".xml");
			writeTemplate(root, file, "ftl/sqlmap.ftl");
			sqlmap.add(sqlmapFolder + "/sqlmap_" + tableClass.getClassName()
					+ ".xml");

		}
		{ // IBATIS 配置文件
			root = new HashMap(0);
			root.put("sqlmap", sqlmap);
			file = new File(basePath + File.separator + "sql-map-config.xml");
			writeTemplate(root, file, "ftl/sql-map-config.ftl");
		}
		{// spring-dao.xml/spring-service.xml
			root = new HashMap(0);
			root.put("tableList", dataTables);
			file = new File(basePath + File.separator + springFolder
					+ File.separator + "spring-dao.xml");
			writeTemplate(root, file, "ftl/spring-dao.ftl");
			file = new File(basePath + File.separator + springFolder
					+ File.separator + "spring-service.xml");
			writeTemplate(root, file, "ftl/spring-service.ftl");
		}
	}

	protected void writeTemplate(Map root, File file, String tempalteFile)
			throws IOException, TemplateException {
		Template tmp = cfg.getTemplate(tempalteFile);
		OutputStream os = new BufferedOutputStream(new FileOutputStream(file));
		Writer out = new OutputStreamWriter(os);
		tmp.process(root, out);
		out.flush();
	}

	protected void mkdirs(String basePath, String packageName) {
		String path = basePath + File.separator
				+ packageName.replace(".", File.separator);
		File folder = new File(path);
		if (!folder.exists()) {
			folder.mkdirs();
		}
	}

	/**
	 * 转换字段名为变量名
	 * 
	 * @param field
	 *            字段名
	 * @param ignoreUnderline
	 *            忽悠下划线
	 * @return 转换后的字段名
	 */
	protected String convertClassField(String filed, boolean ignoreUnderline) {
		return convert(filed, false, ignoreUnderline);
	}

	/**
	 * 转换表名为类名
	 * 
	 * @param tableName
	 *            表名
	 * @return 类名
	 */
	protected String convertClassName(String tableName) {
		return convert(tableName, true, true);
	}

	/**
	 * 将表名/字段名转换为类名/类字段
	 * 
	 * @param name
	 *            表名/字段名
	 * @param upperCaseFiristLetter
	 *            首字母是否大写
	 * @return 转换后的名字
	 */
	protected String convert(String name, boolean upperCaseFiristLetter,
			boolean ignoreUnderline) {
		char[] chars = name.toLowerCase().toCharArray();
		boolean flag = upperCaseFiristLetter;// 是否大写
		StringBuffer buf = new StringBuffer();
		for (int i = 0; i < chars.length; i++) {
			if (chars[i] >= 'a' && chars[i] <= 'z' || i != 0 && chars[i] >= '0'
					&& chars[i] <= '9') {
				buf.append(flag ? String.valueOf(chars[i]).toUpperCase()
						: chars[i]);
				flag = false;
			} else if (chars[i] == '_' && !ignoreUnderline) {
				buf.append(chars[i]);
				flag = false;
			} else {
				flag = true;
			}
		}
		return buf.toString();
	}

	public void setDataSource(DataSource dataSource) {
		this.dataSource = dataSource;
	}

	public void setSelectTables(String selectTables) {
		this.selectTables = selectTables;
	}

	public void setSelectColumns(String selectColumns) {
		this.selectColumns = selectColumns;
	}

	public void setSelectTablePK(String selectTablePK) {
		this.selectTablePK = selectTablePK;
	}

	public void setSqlmapFolder(String sqlmapFolder) {
		this.sqlmapFolder = sqlmapFolder;
	}
}
